在程式中,傳遞內容基本上有兩種方法,一種是傳值,另一種則是傳址(又稱傳參):
object
外的任意基本型別,非變數也非物件屬性的「純值」。使用傳址的好處是節省記憶體,不需要開闢那麼多額外空間,而且使用非常方便,只要對應同個參考就好,如果多個地方共用相同的內容,要修改時也只需要更改一處,非常方便。
與此相對的,由於記憶體指向的是同一個位置,容易不小心改變某個由多個物件共用參考的內容,而非專屬於該物件的內容。
前面已經提過,JS 物件實際上並不儲存任何值,而是將這些值的記憶體參考作為「屬性」存在物件內部,也就是所謂的「傳址/傳參」。
因此在複製物件時,如果僅複製記憶體參考,兩個物件內部的值實質上是指向同一個記憶體空間,就稱為「淺拷貝」。
而「深拷貝」則是將原本的值本身完整複製出來,並放到另一個記憶體空間,其結果會與原複製物件指向完全不同的參考。
淺拷貝是 JS 中物件最常見的複製方式,參考以下範例:
function anotherFunction() { /*..*/ }
var anotherObject = {
c: true
};
var anotherArray = [];
var myObject = {
a: 2, // 純值
b: anotherObject, // 參考
c: anotherArray, // 參考
d: anotherFunction // 參考
};
上面程式碼中,我們最後得到了一個新物件 myObject
,它的 a
是個數字純值,但 b
、c
、d
都只是指向內容儲存位置的參考,因此這三個屬性的內容都屬於淺拷貝。
以下就來介紹一些淺拷貝的常見方式:
使用 Object.assign()
時,可以複製一個或多個物件自身的屬性到另一個目標物件上,回傳值為該目標物件。
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 },回傳一個新物件
console.log(o1); // { a: 1, b: 2, c: 3 },目標物件本身也被改變
當合併物件包含同名屬性時,後傳入的物件屬性會覆蓋前面:
var o1 = { a: 1, b: 1, c: 1 };
var o2 = { b: 2, c: 2 };
var o3 = { c: 3 };
var obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
傳入 undefined
和 null
作為參數時會被忽視:
var o1 = { a: 1 };
var obj = Object.assign({}, null, undefined, o1);
console.log(obj); // { a: 1 }
傳入基本類型作為參數時,只有可枚舉的類型(字串)會被複製:
var o1 = Object.assign({}, "abc");
var o2 = Object.assign({}, true);
var o3 = Object.assign({}, 10);
var o4 = Object.assign({}, Symbol('foo'));
console.log(o1); // { '0': 'a', '1': 'b', '2': 'c' }
console.log(o2); // {}
console.log(o3); // {}
console.log(o4); // {}
使用展開運算符「...
」是另一種快速複製物件的方法,具體用法可參考這篇文章。
由於物件可以有多層形式,在許多常見的狀況下,物件的拷貝經常都是淺拷貝,複製的是記憶體參考而非值本身。
如果要進行深拷貝,一個最常見且安全的方式是使用 JSON 格式轉換。也就是將一個物件序列化為一個 JSON 字串,之後再重新解析為擁有相同結構和值的物件:
var obj = { a: "A" };
var jsonStr = JSON.stringify(obj);
var newObj = JSON.parse(jsonStr);
console.log(obj); // { a: 'A' }
console.log(newObj); // { a: 'A' }
console.log(obj === newObj); // false